查看原文
其他

实战版本更新(okhttp3、service、notification)

BmobSnail 鸿洋 2019-04-05

每日推荐


跟miui一样的自动滚动截屏

https://github.com/android-notes/auto-scroll-capture


本文作者


作者:BmobSnail

链接:

https://www.jianshu.com/p/b669940c9f3e

本文由作者授权发布。


1前言


整理功能,把这块拿出来单独做个demo,好和大家分享交流一下。

版本更新这个功能一般 app 都有实现,而用户获取新版本一般来源有两种:


  • 一种是各种应用市场的新版本提醒

  • 一种是打开app时拉取版本信息

  • (还要一种推送形式,热修复或打补丁包时用得多点)


这两区别就在于,市场的不能强制更新、不够及时、粘度低、单调。


摘要


下面介绍这个章节,你将会学习或复习到一些技术:


  •  dialog 实现 KeyEventListener 的重写,在弹窗后用户不能通过点击虚拟后退键关闭窗口

  • 忽略后不再提示,下个版本更新再弹窗

  • 自定义 service 来承载下载功能

  • okhttp3 下载文件到 sdcard,文件权限判断

  • 绑定服务,实现回调下载进度

  • 简易的 mvp 架构

  • 下载完毕自动安装




这个是我们公司的项目,有版本更新时的截图。当然,我们要实现的demo不会写这么复杂的ui。


功能点(先把demo的最终效果给上看一眼)



1Dialog


dialog.setCanceledOnTouchOutside() 触摸窗口边界以外是否关闭窗口,设置 false 即不关闭。


dialog.setOnKeyListener() 设置KeyEvent的回调监听方法。如果事件分发到dialog的话,这个事件将被触发,一般是在窗口显示时,触碰屏幕的事件先分发到给它,但默认情况下不处理直接返回false,也就是继续分发给父级处理。


如果只是拦截返回键就只需要这样写:


mDialog.setOnKeyListener(
       new DialogInterface.OnKeyListener() {
   @Override
   public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
       return keyCode == KeyEvent.KEYCODE_BACK &&
                     mDialog != null && mDialog.isShowing();
   }
});


2忽略版本更新


忽略本次版本更新,不再弹窗提示

下次有新版本时,再继续弹窗提醒


其实这样的逻辑很好理解,并没有什么特别的代码。比较坑的是,这里往往需要每次请求接口才能判断到你app是否已经是最新版本。


这里我并没有做网络请求,只是模拟一下得到的版本号,然后做一下常规的逻辑判断,在我们项目中,获取版本号只能通过请求接口来得到,也就是说每次启动请求更新的接口,也就显得非常浪费,我是建议把这个版本号的在你们的首页和其它接口信息一起返回,然后写入在 SharedPreferences。


每次先判断与忽略的版本是否一样,一样则跳过,否则下次启动时请求更新接口


public void checkUpdate(String local) {
   //假设获取得到最新版本
   //一般还要和忽略的版本做比对。。这里就不累赘了
   String version = "2.0";
   String ignore = SpUtils.getInstance().getString("ignore");
   if (!ignore.equals(version) && !ignore.equals(local)) {
       view.showUpdate(version);
   }
}


3自定义service


这里需要和 service 通讯,我们自定义一个绑定的服务,需要重写几个比较关键的方法,分别是 onBind(返回和服务通讯的频道IBinder)、unbindService(解除绑定时销毁资源)、和自己写一个 Binder 用于通讯时返回可获取service对象。进行其它操作。


context.bindService(context,conn,flags)


  • context 上下文

  • conn(ServiceConnnetion),实现了这个接口之后会让你实现两个方法onServiceConnected(ComponentName, IBinder) 也就是通讯连通后返回我们将要操作的那个 IBinder 对象、onServiceDisconnected(ComponentName) 断开通讯

  • flags 服务绑定类型,它提供很多种类型,但常用的也就这里我我们用到的是 Service.BIND_AUTO_CREATE, 源码对它的描述大概意思是说,在你确保绑定此服务,就自动启动服务。(意思就是说,你bindService之后,传的不是这个参数,有可能你的服务就没反应咯)


通过获取这个对象就可以对 service 进行操作了。这个自定义service篇幅比较长,建议下载demo下来仔细阅读一番.


public class DownloadService extends Service {
   //定义notify的id,避免与其它的notification的处理冲突
   private static final int NOTIFY_ID = 0;
   private static final String CHANNEL = "update";
   private DownloadBinder binder = new DownloadBinder();
   private NotificationManager mNotificationManager;
   private NotificationCompat.Builder mBuilder;
   private DownloadCallback callback;
   //定义个更新速率,避免更新通知栏过于频繁导致卡顿
   private float rate = .0f;
   @Nullable
   @Override
   public IBinder onBind(Intent intent) {
       return binder;
   }
   @Override
   public void unbindService(ServiceConnection conn) {
       super.unbindService(conn);
       mNotificationManager.cancelAll();
       mNotificationManager = null;
       mBuilder = null;
   }
   /**
    * 和activity通讯的binder
    */

   public class DownloadBinder extends Binder{
       public DownloadService getService(){
           return DownloadService.this;
       }
   }
   /**
    * 创建通知栏
    */

   private void setNotification() {
       if (mNotificationManager == null)
           mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
       mBuilder = new NotificationCompat.Builder(this,CHANNEL);
       mBuilder.setContentTitle("开始下载")
               .setContentText("正在连接服务器")
               .setSmallIcon(R.mipmap.ic_launcher_round)
               .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
               .setOngoing(true)
               .setAutoCancel(true)
               .setWhen(System.currentTimeMillis());
       mNotificationManager.notify(NOTIFY_ID, mBuilder.build());
   }
   /**
    * 下载完成
    */

   private void complete(String msg) {
       if (mBuilder != null) {
           mBuilder.setContentTitle("新版本").setContentText(msg);
           Notification notification = mBuilder.build();
           notification.flags = Notification.FLAG_AUTO_CANCEL;
           mNotificationManager.notify(NOTIFY_ID, notification);
       }
       stopSelf();
   }
   /**
    * 开始下载apk
    */

   public void downApk(String url,DownloadCallback callback) {
       this.callback = callback;
       if (TextUtils.isEmpty(url)) {
           complete("下载路径错误");
           return;
       }
       setNotification();
       handler.sendEmptyMessage(0);
       Request request = new Request.Builder().url(url).build();
       new OkHttpClient().newCall(request).enqueue(new Callback() {
           @Override
           public void onFailure(Call call, IOException e) {
               Message message = Message.obtain();
               message.what = 1;
               message.obj = e.getMessage();
               handler.sendMessage(message);
           }
           @Override
           public void onResponse(Call call, Response response) throws IOException {
               if (response.body() == null) {
                   Message message = Message.obtain();
                   message.what = 1;
                   message.obj = "下载错误";
                   handler.sendMessage(message);
                   return;
               }
               InputStream is = null;
               byte[] buff = new byte[2048];
               int len;
               FileOutputStream fos = null;
               try {
                   is = response.body().byteStream();
                   long total = response.body().contentLength();
                   File file = createFile();
                   fos = new FileOutputStream(file);
                   long sum = 0;
                   while ((len = is.read(buff)) != -1) {
                       fos.write(buff,0,len);
                       sum+=len;
                       int progress = (int) (sum * 1.0f / total * 100);
                       if (rate != progress) {
                           Message message = Message.obtain();
                           message.what = 2;
                           message.obj = progress;
                           handler.sendMessage(message);
                           rate = progress;
                       }
                   }
                   fos.flush();
                   Message message = Message.obtain();
                   message.what = 3;
                   message.obj = file.getAbsoluteFile();
                   handler.sendMessage(message);
               } catch (Exception e) {
                   e.printStackTrace();
               } finally {
                   try {
                       if (is != null)
                           is.close();
                       if (fos != null)
                           fos.close();
                   } catch (Exception e) {
                       e.printStackTrace();
                   }
               }
           }
       });
   }
   /**
    * 路径为根目录
    * 创建文件名称为 updateDemo.apk
    */

   private File createFile() {
       String root = Environment.getExternalStorageDirectory().getPath();
       File file = new File(root,"updateDemo.apk");
       if (file.exists())
           file.delete();
       try {
           file.createNewFile();
           return file;
       } catch (IOException e) {
           e.printStackTrace();
       }
       return null ;
   }
   /**
    * 把处理结果放回ui线程
    */

   private Handler handler = new Handler(new Handler.Callback() {
       @Override
       public boolean handleMessage(Message msg) {
           switch (msg.what) {
               case 0:
                   callback.onPrepare();
                   break;
               case 1:
                   mNotificationManager.cancel(NOTIFY_ID);
                   callback.onFail((String) msg.obj);
                   stopSelf();
                   break;
               case 2:{
                   int progress = (int) msg.obj;
                   callback.onProgress(progress);
                   mBuilder.setContentTitle("正在下载:新版本...")
                           .setContentText(String.format(Locale.CHINESE,"%d%%",progress))
                           .setProgress(100,progress,false)
                           .setWhen(System.currentTimeMillis());
                   Notification notification = mBuilder.build();
                   notification.flags = Notification.FLAG_AUTO_CANCEL;
                   mNotificationManager.notify(NOTIFY_ID,notification);}
                   break;
               case 3:{
                   callback.onComplete((File) msg.obj);
                   //app运行在界面,直接安装
                   //否则运行在后台则通知形式告知完成
                   if (onFront()) {
                       mNotificationManager.cancel(NOTIFY_ID);
                   } else {
                       Intent intent = installIntent((String) msg.obj);
                       PendingIntent pIntent = PendingIntent.getActivity(getApplicationContext()
                       ,0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
                       mBuilder.setContentIntent(pIntent)
                               .setContentTitle(getPackageName())
                               .setContentText("下载完成,点击安装")
                               .setProgress(0,0,false)
                               .setDefaults(Notification.DEFAULT_ALL);
                       Notification notification = mBuilder.build();
                       notification.flags = Notification.FLAG_AUTO_CANCEL;
                       mNotificationManager.notify(NOTIFY_ID,notification);
                   }
                   stopSelf();}
                   break;
           }
           return false;
       }
   });
   /**
    * 是否运行在用户前面
    */

   private boolean onFront() {
       ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
       List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
       if (appProcesses == null || appProcesses.isEmpty())
           return false;
       for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
           if (appProcess.processName.equals(getPackageName()) &&
                   appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
               return true;
           }
       }
       return false;
   }
   /**
    * 安装
    * 7.0 以上记得配置 fileProvider
    */

   private Intent installIntent(String path){
       try {
           File file = new File(path);
           String authority = getApplicationContext().getPackageName() + ".fileProvider";
           Uri fileUri = FileProvider.getUriForFile(getApplicationContext(), authority, file);
           Intent intent = new Intent(Intent.ACTION_VIEW);
           intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
           if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
               intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
               intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
           } else {
               intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
           }
           return intent;
       } catch (Exception e) {
           e.printStackTrace();
       }
       return null;
   }
   /**
    * 销毁时清空一下对notify对象的持有
    */

   @Override
   public void onDestroy() {
       mNotificationManager = null;
       super.onDestroy();
   }
   /**
    * 定义一下回调方法
    */

   public interface DownloadCallback{
       void onPrepare();
       void onProgress(int progress);
       void onComplete(File file);
       void onFail(String msg);
   }
}


4okhttp3下载文件


看通透事情的本质,你就可以为所欲为了。怎么发起一个 okhttp3 最简单的请求,看下面!简洁明了吧,这里抽离出来分析一下,最主要还是大家的业务、框架、需求都不一样,所以节省时间看明白写入逻辑就好了,这样移植到自己项目的时候不至于无从下手。


明白之后再结合比较流行常用的如 Retrofit、Volley之类的插入这段就好了。避免引入过多的第三方库而导致编译速度变慢,项目臃肿嘛。


我们来看回上面的代码,看到 downApk 方法,我是先判断路径是否为空,为空就在通知栏提示用户下载路径错误了,这样感觉比较友好。


判断后就创建一个 request 并执行这个请求。很容易就理解了,我们要下载apk,只需要一个 url 就足够了是吧(这个url一般在检测版本更新接口时后台返回)。然后第一步就配置好了,接下来是处理怎么把文件流写出到 sdcard。


  • 写入:是指读取文件射进你app内(InputStream InputStreamReader FileInputStream BufferedInputStream)

  • 写出:是指你app很无赖的拉出到sdcard(OutputStream OutputStreamWriter FileOutputStream BufferedOutputStream)


仅此送给一直对 input、ouput 记忆混乱的同学


Request request = new Request.Builder().url(url).build();
new OkHttpClient().newCall(request).enqueue(new Callback() {});


写出文件


InputStream is = null;
byte[] buff = new byte[2048];
int len;
FileOutputStream fos = null;
try {
      is = response.body().byteStream();             //读取网络文件流
      long total = response.body().contentLength();  //获取文件流的总字节数
      File file = createFile();                      //自己的createFile() 在指定路径创建一个空文件并返回
      fos = new FileOutputStream(file);                        
      long sum = 0;
      while ((len = is.read(buff)) != -1) {          //嘣~嘣~一点一点的往 sdcard &#$%@$%#%$
         fos.write(buff,0,len);
         sum+=len;
         int progress = (int) (sum * 1.0f / total * 100);
         if (rate != progress) {
              //用handler回调通知下载进度的
              rate = progress;
         }
       }
       fos.flush();
       //用handler回调通知下载完成
} catch (Exception e) {
       e.printStackTrace();
} finally {
       try {
              if (is != null)
                  is.close();
              if (fos != null)
                  fos.close();
        } catch (Exception e) {
              e.printStackTrace();
        }
 }


5文件下载回调


在上面的okhttp下载处理中,我注释标注了回调的位置,因为下载线程不再UI线程中,大家需要通过handler把数据先放回我们能操作UI的线程中再返回会比较合理,在外面实现了该回调的时候就可以直接处理数据。


private Handler handler = new Handler(new Handler.Callback() {
   @Override
   public boolean handleMessage(Message msg) {
       switch (msg.what) {
           case 0://下载操作之前的预备操作,如检测网络是否wifi
               callback.onPrepare();
               break;
           case 1://下载失败,清空通知栏,并销毁服务自己
               mNotificationManager.cancel(NOTIFY_ID);
               callback.onFail((String) msg.obj);
               stopSelf();
               break;
           case 2:{//回显通知栏的实时进度
               int progress = (int) msg.obj;
               callback.onProgress(progress);
               mBuilder.setContentTitle("正在下载:新版本...")
                       .setContentText(String.format(Locale.CHINESE,"%d%%",progress))
                       .setProgress(100,progress,false)
                       .setWhen(System.currentTimeMillis());
               Notification notification = mBuilder.build();
               notification.flags = Notification.FLAG_AUTO_CANCEL;
               mNotificationManager.notify(NOTIFY_ID,notification);}
               break;
           case 3:{//下载成功,用户在界面则直接安装,否则叮一声通知栏提醒,点击通知栏跳转到安装界面
               callback.onComplete((File) msg.obj);
               if (onFront()) {
                   mNotificationManager.cancel(NOTIFY_ID);
               } else {
                   Intent intent = installIntent((String) msg.obj);
                   PendingIntent pIntent = PendingIntent.getActivity(getApplicationContext()
                   ,0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
                   mBuilder.setContentIntent(pIntent)
                           .setContentTitle(getPackageName())
                           .setContentText("下载完成,点击安装")
                           .setProgress(0,0,false)
                           .setDefaults(Notification.DEFAULT_ALL);
                   Notification notification = mBuilder.build();
                   notification.flags = Notification.FLAG_AUTO_CANCEL;
                   mNotificationManager.notify(NOTIFY_ID,notification);
               }
               stopSelf();}
               break;
       }
       return false;
   }
});


6自动安装


android 随着版本迭代的速度越来越快,有一些api已经被遗弃了甚至不存在了。7.0 的文件权限变得尤为严格,所以之前的一些代码在高一点的系统可能导致崩溃,比如下面的,如果不做版本判断,在7.0的手机就会抛出FileUriExposedException异常,说app不能访问你的app以外的资源。官方文档建议的做法,是用FileProvider来实现文件共享。


也就是说在你项目的src/res新建个xml文件夹再自定义一个文件,并在配置清单里面配置一下这个:





file_paths.xml


<?xml version="1.0" encoding="utf-8"?>
<paths>
   <external-path
       name="external"
       path=""/>

</paths>


安装apk


try {
   String authority = getApplicationContext().getPackageName() + ".fileProvider";
   Uri fileUri = FileProvider.getUriForFile(this, authority, file);
   Intent intent = new Intent(Intent.ACTION_VIEW);
   intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   //7.0以上需要添加临时读取权限
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
       intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
       intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
   } else {
       Uri uri = Uri.fromFile(file);
       intent.setDataAndType(uri, "application/vnd.android.package-archive");
   }
   startActivity(intent);
   //弹出安装窗口把原程序关闭。
   //避免安装完毕点击打开时没反应
   killProcess(android.os.Process.myPid());
} catch (Exception e) {
   e.printStackTrace();
}


已把 Demo 放在github

https://github.com/BmobSnail/UpdateDemo


希望大家能从中学习到东西,不再困惑.


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读


教你如何一套源码编译多个APP

回顾下今年使用过的好用的插件、工具等



如果你想要跟大家分享你的文章,欢迎投稿~


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存